package org.fjsei.yewu.resolver.sei;

import com.alibaba.fastjson2.JSON;
import com.querydsl.core.BooleanBuilder;
import graphql.relay.Connection;
import graphql.relay.SimpleListConnection;
import graphql.schema.DataFetchingEnvironment;
import md.cm.base.Persons;
import md.cm.geography.*;
import md.cm.unit.QUnit;
import md.cm.unit.Unit;
import md.cm.unit.Units;
import md.specialEqp.*;
import md.specialEqp.inspect.*;
import md.specialEqp.type.PipingUnit;
import md.specialEqp.type.PipingUnitRepository;
import md.specialEqp.type.QPipingUnit;
import md.system.Authority;
import md.system.AuthorityRepository;
import md.system.User;
import md.system.UserRepository;
//import org.elasticsearch.action.search.SearchRequest;
//import org.elasticsearch.action.search.SearchResponse;
//import org.elasticsearch.index.query.*;
//import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.fjsei.yewu.aop.MetricsLogger;
import org.fjsei.yewu.entity.fjtj.*;
import org.fjsei.yewu.filter.Node;
import org.fjsei.yewu.filter.SimpleReport;
import org.fjsei.yewu.filter.UserBase;
import org.fjsei.yewu.graphql.DbPageConnection;
import org.fjsei.yewu.index.*;
import org.fjsei.yewu.input.*;
//import org.fjsei.yewu.jpa.ModelFiltersImpl;
import org.fjsei.yewu.jpa.PageOffsetFirst;
import org.fjsei.yewu.resolver.Comngrql;
import org.fjsei.yewu.security.JwtUser;
import org.fjsei.yewu.security.JwtUserDetailsService;
import org.fjsei.yewu.util.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
import org.springframework.data.elasticsearch.core.*;
//import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
//import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.repository.CrudRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.*;
import java.util.*;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;

import static graphql.Assert.assertShouldNeverHappen;
import static graphql.Assert.assertTrue;
import static java.lang.Long.parseLong;
//import static org.springframework.data.elasticsearch.client.elc.Queries.termQuery;这个包实际就是简单的扩展，把函数式改成普通的。?感觉意义不大。
//import static org.elasticsearch.index.query.QueryBuilders.*;


//import org.springframework.data.jpa.repository.EntityGraph;   简名同名字冲突
//@Transactional类完全限定名：而不是javax.的那一个。
//import javax.transaction.Transactional;
//import java.sql.Date;


//实际相当于controller;一般都有MutationResolver反而QueryResolver不一定需要，@Autowired最好能够同类功能汇集，避免每个模块都上很多xxxRepository注入。
//这个类名字不能重复简明！
//graphQL安全性(query/mutation返回的对象可以进行id关联嵌套查询，如何控制关联信息访问)，这方面apollo做的较好：@注释扩展。
//信息安全私密字段不建议用graphQL的关联嵌套=内省查询，独立配合用REST接口也是候选方式。
//这里接口函数比graphqls模型多出了也没关系。
/**基础公用的graphQL接口:
 * */
@Controller
@Transactional(readOnly = true)         //(readOnly = true) 查询60秒就超时。
public class BaseQuery extends Comngrql  {
    /**node() 接口函数映射的DB模型，提高性能*/
    final Map<String, CrudRepository>  nodePools= new HashMap<>();
    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;
    @Autowired
    private Equipments eQPRepository;
    @Autowired
    private EqpEsRepository eqpEsRepository;
    @Autowired
    private CompanyEsRepository companyEsRepository;
    @Autowired
    private PersonEsRepository personEsRepository;
    @Autowired
    private IspRepository iSPRepository;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private TaskRepository taskRepository;
    @Autowired
    private ReportRepository reportRepository;
    @Autowired
    private Units units;
    @Autowired
    private AuthorityRepository authorityRepository;

    @Autowired
    private EqpMgeRepository eqpMgeRepository;
    @Autowired
    private ElevParaRepository elevParaRepository;
    @Autowired
    private HouseMgeRepository houseMgeRepository;
    @Autowired
    private UntMgeRepository untMgeRepository;
    @Autowired
    private DictEqpTypeRepository dictEqpTypeRepository;
    @Autowired  private PipingUnitRepository pipingUnitRepository;
    @Autowired  private CountryRepository countryRepository;
    @Autowired  private ProvinceRepository provinceRepository;
    @Autowired  private CountyRepository countyRepository;
    @Autowired  private CityRepository cityRepository;
    @Autowired  private AdminunitRepository adminunitRepository;
    @Autowired private TownRepository townRepository;
    @Autowired   private Persons persons;
    @Autowired  private DetailRepository detailRepository;
    @Autowired  private  Agreements agreements;
    @Autowired  private  Requests requests;

    @PersistenceContext(unitName = "entityManagerFactorySei")
    private EntityManager emSei;
    @Autowired
    private ElasticsearchOperations esTemplate;

    //只运行一次的：Autowired必须先做
    /**所有的graphQl实体模型的Type名称映射到Repository； 支持node(id)访问：@保密性授权？
     * */
    @PostConstruct
    private void init() {
        nodePools.put("Report",reportRepository);
        nodePools.put("Task",taskRepository);
        nodePools.put("PipingUnit",pipingUnitRepository);
        nodePools.put("Eqp",eQPRepository);
        nodePools.put("Country",countryRepository);
        nodePools.put("Province",provinceRepository);
        nodePools.put("City",cityRepository);
        nodePools.put("County",countyRepository);
        nodePools.put("Adminunit",adminunitRepository);
        nodePools.put("Town",townRepository);
        nodePools.put("Detail",detailRepository);
        nodePools.put("Isp",iSPRepository);
        nodePools.put("Unit",units);
        nodePools.put("Agreement",agreements);
        nodePools.put("Request",requests);

    }

    //前端实际不可能用！把数据库全部都同时查入后端内存，太耗；查询只能缩小范围都得分页查，就算原子更新操作也不能全表一个个update，大的表可执行力太差！
    @Deprecated
    public Iterable<Eqp> findAllEQPs_删除() {
        String partcod = "";
        String partoid = "";
        Pageable pageable = PageOffsetFirst.of(0, 35, Sort.by(Sort.Direction.ASC, "oid"));         //Integer.parseInt(10)
        Page<Eqp> allPage = eQPRepository.findAll(new Specification<Eqp>() {
            @Override
            public Predicate toPredicate(Root<Eqp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> predicateList = new ArrayList<Predicate>();
                if (!StringUtils.isEmpty(partcod)) {
                    Path<String> p = root.get("cod");
                    predicateList.add(cb.like(p, "%" + partcod + "%"));
                }
                if (!StringUtils.isEmpty(partoid)) {
                    Path<String> p = root.get("oid");
                    predicateList.add(cb.like(p, "%" + partoid + "%"));
                }
                predicateList.add(cb.le(root.get("id"), 2100));
                Predicate[] predicates = new Predicate[predicateList.size()];
                predicateList.toArray(predicates);
                query.where(predicates);
                return null;
            }
        }, pageable);
    /*
    //加了EntityGraph反而更慢HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
             EntityGraph graph =emSei.getEntityGraph("Eqp.task");
            //emSei.createQuery("FROM Eqp", Eqp.class).getResultList()没.setHint(?)没法使用缓存的。
            List<Eqp> eqps = emSei.createQuery("FROM Eqp", Eqp.class)
                    .setHint("jakarta.persistence.fetchgraph", graph)
              //      .setFirstResult(11)
             //      .setMaxResults(20)
                    .getResultList();
            //getResultStream().limit(10).collect(Collectors.toList()) 已经全表取到内存来了;
    */
        List<Eqp> eqps = allPage.getContent();
        return eqps;        //.subList(74070,74085);
    }

    //多数系统正常地，查询都是直接规定设计好了参数范围的模式，但是灵活性较差，参数个数和逻辑功能较为限制；但安全性好，就是代码上麻烦点。
    public Iterable<Equipment> findEQPLike(DeviceCommonInput filter) {
        List<Equipment> allrt = new ArrayList<Equipment>();
        List<Eqp> list = eQPRepository.findAll();
        list.forEach(item -> {
            allrt.add(item);
        });
        return allrt;
    }

    //orderBy 可支持直接指定某属性的下级字段。 {"orderBy": "pos.building",}
    public Iterable<Equipment> findAllEQPsFilterInput(DeviceCommonInput filter, int offset, int first, String orderBy, boolean asc) {
        Pageable pageable;

        if (StringUtils.isEmpty(orderBy))
            pageable = PageOffsetFirst.of(offset, first);
        else
            pageable = PageOffsetFirst.of(offset, first, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));
        Page<Eqp> allPage = eQPRepository.findAll(new Specification<Eqp>() {
            @Override
            public Predicate toPredicate(Root<Eqp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> predicateList = new ArrayList<Predicate>();
                if (!StringUtils.isEmpty(filter.getCod())) {
                    //   Path<Task> p = root.get("task");
                    Path<String> p = root.get("cod");
                    //非集合型的关联对象可以直接指定下一级的字段SingularAttribute。　该属性，Set，List，Map；//最新的一条TASK/Isp,规化成非集合。
                    //PluralAttribute 复数形式Set[];
                    //          p = root.get("pos");
                    //     Path<String> p2 = p.get("devs");
                    predicateList.add(cb.like(p, "%" + filter.getCod() + "%"));
                    // p = root.get("isps");
                }
                if (!StringUtils.isEmpty(filter.getOid())) {
                    Path<String> p = root.get("fNo");
                    predicateList.add(cb.like(p, "%" + filter.getOid() + "%"));
                }
                Predicate[] predicates = new Predicate[predicateList.size()];
                predicateList.toArray(predicates);
                query.where(predicates);
                return null;
            }
        }, pageable);

        List<Equipment> allrt = new ArrayList<Equipment>();
        List<Eqp> eqps = allPage.getContent();
        eqps.forEach(item -> {
            allrt.add(item);
        });
        return allrt;
    }

    public long countAllEQPsFilter(DeviceCommonInput filter) {
        Specification<Eqp> spec = new Specification<Eqp>() {
            @Override
            public Predicate toPredicate(Root<Eqp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> predicateList = new ArrayList<Predicate>();
                if (!StringUtils.isEmpty(filter.getCod())) {
                    Path<String> p = root.get("type");
                    predicateList.add(cb.like(p, "%" + filter.getCod() + "%"));
                }
                if (!StringUtils.isEmpty(filter.getOid())) {
                    Path<String> p = root.get("fNo");
                    predicateList.add(cb.like(p, "%" + filter.getOid() + "%"));
                }
                Predicate[] predicates = new Predicate[predicateList.size()];
                predicateList.toArray(predicates);
                query.where(predicates);
                return null;
            }
        };
        return eQPRepository.count(spec);
    }

    //权限控制?
    public Iterable<Report> findAllReports() {
        return reportRepository.findAll();
    }

/*    public Iterable<Address> findAllPositions() {
        return addressRepository.findAll();
    }*/

    public Iterable<UserBase> findAllUsers() {
        List<User> users = userRepository.findAll();
        List<UserBase> parents = new ArrayList<UserBase>();
        parents.addAll(users);
        return parents;  //这里返回的对象实际还是User派生类型的，只是graphQL将会把它当成接口类型Person使用。
    }

    //Spring Security——基于表达式的权限控制，Spring 表达式语言(Spring EL)；SpEL 操作符; 正则表达式匹配?
    //@PreAuthorize("hasRole('ROLE_'.concat(this.class.simpleName))")
    //SpEL怎样从List、Map集合中取值; @Value("#{numberBean.no == 999 and numberBean.no < 900}")
    // @PreAuthorize("hasRole('ROLE_USER') and hasIpAddress('localhost')" )
    @PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
    public Iterable<Authority> findAllAuthority() {
        //todo: 不能乱用Authority 安全！控制
        return authorityRepository.findAll();
    }

    public Iterable<User> findUserLike(String username) {
        return userRepository.findAllByUsernameLike(username);
    }
    //必须注解上2种的  @Argument String id 报错org.springframework.graphql.data.GraphQlArgumentBinder$ArgumentsBindingResult: 1 errors 前端也需重新深层类型定义文件
    @QueryMapping
    public User getUser(@Argument String id) {
        Tool.ResolvedGuuid gId= Tool.fromGluuId(id);
        if(gId.getType().equals("User") ){
            return userRepository.findById(gId.getId()).orElse(null);
        }
        return null;
    }

    public Connection<User> findUserLikeInterface(ComplexInput input, DataFetchingEnvironment env) {
        List<User> users = userRepository.findAllByUsernameLike(input.getUsername());
        return new SimpleListConnection<User>(users).get(env);
    }

    /*
     public Iterable<User> findUserLikeInterface(ComplexInput input) {
         return userRepository.findAllByUsernameLike(input.getUsername());
     }
     public Iterable<Person> findUserLikeInterfacePerson(ComplexInput input) {
       //  return userRepository.findAllByUsernameLike(input.getUsername());
         return null;
     } */
    public UserBase userBasic(UUID id) {
        return userRepository.findById(id).orElse(null);
    }


    public Long countReport(UUID ispId) {
        if (ispId == null) return reportRepository.count();
        Isp isp = iSPRepository.findById(ispId).orElse(null);
        Assert.isTrue(isp != null, "未找到isp:" + isp);
        int myInt = reportRepository.findByIsp(isp).size();
        return parseLong(new String().valueOf(myInt));
    }

/*    public Long countPositionEQP(Long Id) {
        Address position = addressRepository.findById(Id).orElse(null);
        int myInt = position.getEqps().size();
        return parseLong(new String().valueOf(myInt));
    }*/

    //原型对应：getDevice(id:ID!): Eqp! 尽量不要返回值必选的，前端会报错，改成getDevice(id:ID!): EQP可选null。
    public Eqp getDevice(UUID id) {
        return eQPRepository.findById(id).orElse(null);
        //因为LAZY所以必须在这里明摆地把它预先查询出来，否则graphQL内省该字段就没结果=报错; open-in-view也没效果。
        //单层eqp.getTask().stream().count();  //.collect(Collectors.toSet())
        //两层eqp.getTask().stream().forEach(t->t.getIsps().stream().count());
    }

    public EqpEs getEqpEs(UUID id) {
        return eqpEsRepository.findById(id).orElse(null);
    }

    //前端路由乱来？不是正常的url也来这里了： java.lang.Long` from String "favicon.ico": not a valid Long value
    /**尽管用spring for Graphql:可像这种node(id):Type{..}需求还是需要这么处置的, 任何查询都需要一个入口，内省{}可省事点;
     * 但是若后端为了性能用了实体投影机制Projection，必然约束到前端可以内省的关联属性，@是个矛盾！性能 PK 方便前端解耦和随意。
     * */
    @QueryMapping
    @Transactional
    public Equipment getDeviceSelf(@Argument String id) {
        Tool.ResolvedGuuid globalId = Tool.fromGluuId(id);
        Eqp eqp = eQPRepository.findById(globalId.getId()).orElse(null);
        //不预先读取 竟然报错 String names=eqp.getOwner().getPerson().getName();
        //必须倒腾一手,否则前端收不到Eqp子类Elevator的参数。不能直接用Eqp,应该返回接口类型才可以。
        return (Equipment) eqp;
    }

    //选择集安全域的 测试：
    //适应安全域考虑，把结果集造型成特定的过滤基类。
    public Iterable<SimpleReport> findAllBaseReports() {
        List<Report> reps = reportRepository.findAll();
        List<SimpleReport> parents = new ArrayList<SimpleReport>();
        parents.addAll(reps);
        return parents;
    }

    public Equipment findEQPbyCod(String cod) {
        return (Equipment) eQPRepository.findByCod(cod);
    }

//    public Iterable<CompanyEs> findUnitbyNameAnd(String name, String name2) {
//        if (name2 == null) name2 = "";
//        Iterable<CompanyEs> list = companyEsRepository.findAllByNameQueryPhrase2(name, name2);
//        return list;
//    }

//    public Iterable<CompanyEs> findUnitbyNameAnd2(String name, String name2) {
//        Iterable<CompanyEs> list = companyEsRepository.findAllByNameSqueryPhrase2(name, name2);
//        return list;
//    }

//    public Iterable<CompanyEs> findUnitbyName(String name) {
//        Iterable<CompanyEs> list = companyEsRepository.findAllByNameContains(name);
//        return list;
//    }

//    public Iterable<CompanyEs> findUnitbyName1(String name) {
//        Iterable<CompanyEs> list = companyEsRepository.findAllByName_KeywordContains(name);
//        return list;
//    }

//    public Iterable<PersonEs> findUnitbyName2(String name) {
//        Iterable<PersonEs> list = personEsRepository.findAllByNameMatchePhrase(name);
//        return list;
//    }
    /**必须注解才能对外提供接口 @Argument注解的参数不能直接上String[] names的，必须改成包装对象ArrayList<String> names;
     * 结果太多的报错：Result window is too large, from + size must be less than or equal to: [10000] but was
     * */
//    @QueryMapping
//    public Iterable<CompanyEs> findUnitbyNameArr(@Argument ArrayList<String> names) {
//        Iterable<CompanyEs> list = companyEsRepository.findAllByNameIn(names.toArray(new String[names.size()]));
//        return list;
//    }


//    public Iterable<CompanyEs> getCompanyEsbyFilter_旧版本(UnitCommonInput as, Pageable pageable) {
//        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//        MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.
//                matchPhraseQuery("name", as.getName()).slop(5);
//        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("date").from("2016-01-01 00:00:00");
//        BoolQueryBuilder childBoolQueryBuilder = new BoolQueryBuilder().must(matchPhraseQueryBuilder);
//        boolQueryBuilder.must(childBoolQueryBuilder).must(rangeQueryBuilder);
//        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//        searchSourceBuilder.query(childBoolQueryBuilder);
//        searchSourceBuilder.from((int) pageable.getOffset());
//        searchSourceBuilder.size(pageable.getPageSize());
//        SearchRequest searchRequest = new SearchRequest();
//        searchRequest.indices("company");
//        searchRequest.source(searchSourceBuilder);
//        SearchResponse searchResponse = esTemplate.execute(
//                client -> client.search(searchRequest, RequestOptions.DEFAULT)
//        );
//        int size = searchResponse.getHits().getHits().length;
//        return null;   //这种方式其实也会主动返回总数的！　"hits":{"total":{"value":13,]
//    }

    public List<?> getPersonbyFilter(UnitCommonInput as, Pageable pageable) {
        QUnit qm = QUnit.unit;
        BooleanBuilder builder = new BooleanBuilder();
        builder.and(qm.person.name.like(as.getName()+'%'));
        Slice<Unit> rpage= (Slice<Unit>) units.findAll(builder,pageable);
        List<Unit> listO=(List<Unit>) rpage.toList();
       return listO;
    }

    //测试
    public Connection<Unit> getUnitFilter(UnitCommonInput where, int first, String after, int last, String before, String orderBy, boolean asc, DataFetchingEnvironment env) {
        User user = checkAuth();
        if (user == null) return null;
        DbPageConnection<Isp> connection = new DbPageConnection(env);
        int offset = connection.getOffset();
        int limit = connection.getLimit();
        Pageable pageable;
        if (!StringUtils.hasLength(orderBy))
            pageable = PageOffsetFirst.of(offset, limit);
        else
            pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));

        if (where == null) return null;
        List units;
/*        if (where.isCompany())
            units = getCompanyEsbyFilter(where, pageable);
        else*/
            units = getPersonbyFilter(where, pageable);

        return connection.setListData(units).get(env);
    }
    //支持未登录就能查询角色{}，免去控制introsepction逻辑麻烦，把函数的输出自定义改装成普通的JSON字符串/好像REST那样的接口。
    @Deprecated
    @QueryMapping
    public String auth() {
        User user = checkAuth();
        if (user == null || !user.getEnabled()) return "{}";        //未登录或者未正常开通使用的就{}
        //只需要很小部分的User内容输出。
//        User out = user.cloneAuth();      //user这里不可以直接用JSON.toJSONString(user)，会报错，有些字段LAZY。
//        String strJson = JSON.toJSONString(out);
        String strJson="{\"id\": \"12\", \"username\": \"herzhang\", \"authorities\": [ { \"name\": \"ROLE_USER\"  },{ \"name\": \"ROLE_ADMIN\"  } ], \"dep\": \"709\"}";
        return strJson;
    }

    /**时刻都需要验证前端用户的身份；Relay缓存机制防止太勤快查询后端
     * 有些敏感信息直接setXx(null),就能避免给前端泄露的漏洞，还能确保user信息没有被真正变更！Query接口;
     *AuthenticationPrincipal(expression="jwtUser") JwtUser x 報錯：spel.SpelEvaluationException: EL1008E: Property or field 'jwtUser' cannot be found on object of type 'org.fjsei.yewu.security.JwtUser' - maybe not public；
     *若用User authUser(Principal principal) {}注入模式也可以，但是后面(JwtUser)principal).isEnabled()會報錯！
     * 旧版本(@AuthenticationPrincipal JwtUser jwtUser)可以接受到；
     * 改成(@AuthenticationPrincipal org.springframework.security.core.userdetails.User jwtUser)才能接受到？
     * */
    @QueryMapping
    public User authUser(@AuthenticationPrincipal JwtUser jwtUser) {
        //沒有登录的jwtUser=null;可是Principal principal注入模式却是principal!=null,=匿名角色的。
        //测试第一代平台 return   userRepository.findByUsername("herzhang");
        if (jwtUser == null || !jwtUser.isEnabled())  return null;
        User user = checkAuth();
        Set<Authority> authorities=user.getAuthorities();
        user.setAuthorities(null);      //会直接清空的！【务必】@Transactional(readOnly = true)
        return user;
    }

    public Iterable<Equipment> getAllEQP() {
        String partcod = "05T1";
        String partoid = "C8456";
        Specification<Eqp> spec = new Specification<Eqp>() {
            @Override
            public Predicate toPredicate(Root<Eqp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> predicateList = new ArrayList<Predicate>();
                if (!StringUtils.isEmpty(partcod)) {
                    Path<String> p = root.get("cod");
                    predicateList.add(cb.like(p, "%" + partcod + "%"));
                }
                if (!StringUtils.isEmpty(partoid)) {
                    Path<String> p = root.get("oid");
                    predicateList.add(cb.like(p, "%" + partoid + "%"));
                }
                Predicate[] predicates = new Predicate[predicateList.size()];
                predicateList.toArray(predicates);
                query.where(predicates);
                return null;
            }
        };
        Sort sort = Sort.by(Sort.Order.asc("oid"), Sort.Order.desc("id"));
        List<Equipment> allrt = new ArrayList<Equipment>();
        List<Eqp> list = eQPRepository.findAll(spec, sort);
        list.forEach(item -> {
            allrt.add(item);
        });
        return allrt;
    }

    public long countAllEQP() {
        String partcod = "L";
        String partoid = "1";
        Specification<Eqp> spec = new Specification<Eqp>() {
            @Override
            public Predicate toPredicate(Root<Eqp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> predicateList = new ArrayList<Predicate>();
                if (!StringUtils.isEmpty(partcod)) {
                    Path<String> p = root.get("cod");
                    predicateList.add(cb.like(p, "%" + partcod + "%"));
                }
                if (!StringUtils.isEmpty(partoid)) {
                    Path<String> p = root.get("oid");
                    predicateList.add(cb.like(p, "%" + partoid + "%"));
                }
                Predicate[] predicates = new Predicate[predicateList.size()];
                predicateList.toArray(predicates);
                query.where(predicates);
                return null;
            }
        };
        return eQPRepository.count(spec);
    }



    //普通接口为了安全只好写死代码介入过滤，不能依靠前端的输入参数WhereTree来过滤，前端可被用户随意修改的。
    public Iterable<Equipment> findAllEQPsFilter3(DeviceCommonInput where, int offset, int limit, String orderBy, boolean asc) {
        User user= checkAuth();
        if(user==null)   return null;
        if(limit<=0)   limit=20;
        Pageable pageable;
        if (StringUtils.isEmpty(orderBy))
            pageable = PageOffsetFirst.of(offset, limit);
        else
            pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));

        QEqp qm = QEqp.eqp;
        BooleanBuilder builder = new BooleanBuilder();
        if (!StringUtils.isEmpty(where.getCod()))
            builder.and(qm.cod.contains(where.getCod()));
/*        if (where.getOwnerId()!=null)
            builder.and(qm.owner.id.eq(where.getOwnerId()));*/
        if (!StringUtils.isEmpty(where.getFno()))
            builder.and(qm.fno.contains(where.getFno()));

        List<Equipment>  elevators = new ArrayList<Equipment>();
        //Iterable<Eqp> eqps = eQPRepository.findAll(builder,pageable);
        Iterable<Eqp> eqps = eQPRepository.findAll(pageable);
        eqps.forEach(item -> {
           // if(item instanceof Equipment)
                elevators.add(item);
        });
        return elevators;
    }
    //即使用上缓存，通常不会提高性能，除非大家查的数据是相同的，否则一个参数条件变化就不能命中缓存，作用较为有限。
    public Iterable<Equipment> findAllEQPsFilter(DeviceCommonInput where, int offset, int limit, String orderBy, boolean asc) {
        User user= checkAuth();
        if(user==null)   return null;
        if(limit<=0)   limit=20;
        Pageable pageable;
        if (!StringUtils.hasLength(orderBy))
            pageable = PageOffsetFirst.of(offset, limit);
        else
            pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));

        QEqp qm = QEqp.eqp;
        BooleanBuilder builder = new BooleanBuilder();
        if (!StringUtils.isEmpty(where.getCod()))
            builder.and(qm.cod.contains(where.getCod()));
/* todo:       if (where.getOwnerId()!=null)
            builder.and(qm.owner.id.eq(where.getOwnerId()));*/
        if (!StringUtils.isEmpty(where.getFno()))
            builder.and(qm.fno.contains(where.getFno()));

/*        List<Equipment>  elevators = new ArrayList<Equipment>();
        Iterable<Eqp> eqps = eQPRepository.findAllNc(builder,pageable);
        eqps.forEach(item -> {
            // if(item instanceof Equipment)
            elevators.add(item);
        });*/
        Slice<Eqp> rpage= (Slice<Eqp>)eQPRepository.findAll(builder,pageable);
        return (List<Equipment>) rpage.map(one->(Equipment)one).toList();
    }
    /**设备列表：过滤条件都放入where对象。
     * 无法针对Task/Detail关联进行过滤设备列表。
     * */
    @MetricsLogger
    @QueryMapping
    @PreAuthorize("hasAnyRole('JyUser','Master')")
    public Connection<EqpCommon> getAllEqpEsFilter(@Argument DeviceCommonInput where, @Argument String after, @Argument Integer first,
                     @Argument String orderBy, @Argument Boolean asc, DataFetchingEnvironment env) {
        DbPageConnection<EqpCommon> connection=new DbPageConnection(env);
        int offset=connection.getOffset();
        int limit=connection.getLimit();
        //Object after1=env.getArgument("after"); 抛出异常！ Integer cannot be cast to String
        //todo: int after2=Integer.parseInt(after);  //前端必选after参数为空的：也会送到后端再后端抛异常 Object required to be not null
        Pageable pageable;
        if (!StringUtils.hasText(orderBy))
            pageable = PageOffsetFirst.of(offset, limit);
        else
            pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));

        List<Query> listquerys = new ArrayList<Query>();
        //若.must(termQuery("cod", where.getCod()));只能对getCod是3个字符正好的才能查。must(termQuery("cod_sort", where.getCod()))只能精确匹配等同SQL=''不会模糊的。
        if (StringUtils.hasText(where.getCod()))
            listquerys.add(Query.of(q -> q
                    .matchPhrase(m -> m
                            .field("cod")
                            .query(where.getCod()).slop(9)
                    )
            ));
//  boolQueryBuilder.must(matchPhraseQuery("cod",where.getCod()).slop(9));   //因analyzer="ngram_analyzer"最少3个字符才能查出
        if (StringUtils.hasText(where.getOid()))
            listquerys.add(Query.of(q -> q
                    .matchPhrase(m -> m
                            .field("oid")
                            .query(where.getOid()).slop(6)
                    )
            ));
//  boolQueryBuilder.must(matchPhraseQuery("oid",where.getOid()).slop(6));    //字符数本身较少的，把slop减少一点更精确,模糊性更少了。
        if (StringUtils.hasText(where.getFno()))
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("fno")
                            .value(where.getFno())
                    )
            ));
//  boolQueryBuilder.must(termQuery("fno",where.getFno()));
        if (StringUtils.hasText(where.getUseu())) {
            //前端【单位选择】无法直接选定返回Unit.id字段，只能在后端主动把Company货Person转查关联出Unit ID;首先单位必须定位:Person的一定是某个个人。
            Unit unit=fromInputUnitGlobalID(where.getUseu());
//       boolQueryBuilder.must(termQuery("useu.id", unit.getId().toString()));
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("useu.id")
                            .value(unit.getId().toString())
                    )
            ));
        }
        if (StringUtils.hasText(where.getIspud())) {
            Tool.ResolvedGuuid gId= Tool.fromGluuId(where.getIspud());
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("ispud.id")
                            .value(gId.getId().toString())
                    )
            ));
        }
        //不能查询其它机构的设备？，不是检验员的User用户：前端报检平台的客户端查询设备=归属本单位管理下的设备。前端掌握;厦门委托设备?
        if (StringUtils.hasText(where.getIspu())) {
            Unit unit=fromInputUnitGlobalID(where.getIspu());
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("ispu.id")
                            .value(unit.getId().toString())
                    )
            ));
        }
        //上了分词的搜索ES:使用 match_phrase 最为恰当好用! 为何查不到啊，查出来太多了实际匹配程度也太低了，兼顾两者需求。 termQuery等于关系数据库的精确匹配=。
        if (StringUtils.hasText(where.getAddress()))
            listquerys.add(Query.of(q -> q
                    .matchPhrase(m -> m
                            .field("address")
                            .query(where.getAddress())
                    )
            ));
        if (StringUtils.hasText(where.getPlno()))
            listquerys.add(Query.of(q -> q
                    .matchPhrase(m -> m
                            .field("plno")
                            .query(where.getPlno()).slop(1)
                    )
            ));
//  boolQueryBuilder.must(matchPhraseQuery("plno",where.getPlno()).slop(1));    //1# 无效符，重复1#个数越多的就越优先，完全一样的反而不能在前面啊。
        if (StringUtils.hasText(where.getCert()))
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("cert")
                            .value(where.getCert())
                    )
            ));
        if (StringUtils.hasText(where.getSno()))
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("sno")
                            .value(where.getSno())
                    )
            ));
        if (StringUtils.hasText(where.getRcod()))
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("rcod")
                            .value(where.getRcod())
                    )
            ));
        if (StringUtils.hasText(where.getPlat()))
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("plat")
                            .value(where.getPlat())
                    )
            ));
        if (StringUtils.hasText(where.getVlgName()))
            listquerys.add(Query.of(q -> q
                    .matchPhrase(m -> m
                            .field("vlg.name")
                            .query(where.getVlgName())
                    )
            ));
//   boolQueryBuilder.must(matchPhraseQuery("vlg.name",where.getVlgName()));    //一步搜索替代两步查找再匹配。
        if (StringUtils.hasText(where.getUseuName())) {
//            BoolQueryBuilder    orBuild=new BoolQueryBuilder();
//            orBuild.should(matchPhraseQuery("useu.company.name",where.getUseuName()))
//                    .should(matchPhraseQuery("useu.person.name",where.getUseuName()));
//            boolQueryBuilder.must(orBuild);       //就是And(company.name.match{} OR person.name.match{})二选一匹配的。
            listquerys.add(Query.of(q -> q
                    .bool(p -> p
                            .should(Query.of(c -> c
                                            .matchPhrase(m -> m
                                                    .field("useu.company.name").query(where.getUseuName())
                                            )),
                                    Query.of(c -> c
                                            .matchPhrase(m -> m
                                                    .field("useu.person.name").query(where.getUseuName())
                                            ))
                            )
                    )
            ));
        }
        //预留过滤：设备种类 4个字段：前端还没增加。
        if (StringUtils.hasText(where.getSort()))
            listquerys.add(Query.of(q -> q
                    .term(m -> m
                            .field("sort")
                            .value(where.getSort())
                    )
            ));

        NativeQueryBuilder  nativeQueryBuilder = NativeQuery.builder()
                .withFilter(q -> q.bool(
                                v->v.must(listquerys)
                        )
                );
        NativeQuery query =nativeQueryBuilder.withPageable(pageable).build();
        SearchHits<EqpEs> searchHits = esTemplate.search(query, EqpEs.class);
        List<SearchHit<EqpEs>> hits=searchHits.getSearchHits();
        Iterable<EqpEs> list= (List<EqpEs>) SearchHitSupport.unwrapSearchHits(hits);
        String queryDslStr= Objects.toString(query.getFilter(),"空");
        List<EqpCommon>  elevators = new ArrayList<EqpCommon>();
        list.forEach(item -> {
            elevators.add(item);
        });
        return connection.setListData(elevators).get(env);
    }

    /**管道单元查找
     * */
    public Connection<PipingUnit> findPipingUnit(String pId, PipingUnitInput where, String after, int first, String orderBy, boolean asc, DataFetchingEnvironment env) {
        Tool.ResolvedGuuid gPId= Tool.fromGluuId(pId);
        User user= checkAuth();
        if(user==null)   return null;
        DbPageConnection<PipingUnit> connection=new DbPageConnection(env);
        int offset=connection.getOffset();
        int limit=connection.getLimit();
        //Object after1=env.getArgument("after"); 抛出异常！ Integer cannot be cast to String
        //todo: int after2=Integer.parseInt(after);  //前端必选after参数为空的：也会送到后端再后端抛异常 Object required to be not null
        Pageable pageable;
        if (StringUtils.isEmpty(orderBy))
            pageable = PageOffsetFirst.of(offset, limit);
        else
            pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));

        QPipingUnit qm = QPipingUnit.pipingUnit;
        BooleanBuilder builder = new BooleanBuilder();
        builder.and(qm.pipe.id.eq(gPId.getId()));
        if(StringUtils.hasText(where.getCode()))
            builder.and(qm.code.contains(where.getCode()));
    //报错hibernate.InstantiationException: No default constructor for entity:  : md.specialEqp.type.PipingUnit
        List<PipingUnit> list=pipingUnitRepository.findAll(builder,pageable).toList();
        return connection.setListData(list).get(env);
    }

//    public Iterable<EqpCommon> getAllEqpEsFilter_保留(DeviceCommonInput where, int offset, int limit, String orderBy, boolean asc) {
//        User user= checkAuth();
//        if(user==null)   return null;
//        if(limit<=0)   limit=20;
//        Pageable pageable;
//        if (StringUtils.isEmpty(orderBy))
//            pageable = PageOffsetFirst.of(offset, limit);
//        else
//            pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));
//
//        List<EqpCommon>  elevators = new ArrayList<EqpCommon>();
//        //matchPhraseQuery("fNo",where.getFno()).slop(9)    //matchQuery("fNo",where.getFno())
//        //wildcardQuery("cert.keyword",where.getCert())
//        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(
//                boolQuery().must(
//                        wildcardQuery("cod.keyword",where.getCod())
//                ).must(
//                        wildcardQuery("fNo.keyword",where.getFno())
//                )
//        ).withPageable(pageable).build();
//
//        IndexCoordinates indexCoordinates=esTemplate.getIndexCoordinatesFor(EqpEs.class);
//        // Stream<CompanyEs> list= esTemplate.stream(searchQuery, CompanyEs.class,indexCoordinates);
//        //queryForList(searchQuery, CompanyEs.class,indexCoordinates);
//        SearchHits<EqpEs> searchHits = esTemplate.search(searchQuery, EqpEs.class, indexCoordinates);
//        List<SearchHit<EqpEs>> hits=searchHits.getSearchHits();
//        //Iterable<CompanyEs> list=esTemplate.search(searchQuery);
//        Iterable<EqpEs> list= (List<EqpEs>) SearchHitSupport.unwrapSearchHits(hits);
//        String sql=searchQuery.getQuery().toString();
//
//        list.forEach(item -> {
//            elevators.add(item);
//        });
//        return elevators;
//    }
    //通过ES搜到的Company或Person的id反过来映射unit_ID
    //该接口可以淘汰了 Node( globalID![!数据多个id]! )｛ ｝直接替换就行！但是一个问题，权限控制？如何细分何处逻辑调用查询的？
    //参数类型要求 @Argument Boolean company必须可以null;
    @QueryMapping
    public Unit getUnit(@Argument String esid,@Argument Boolean company) {
        return fromInputUnitGlobalID(esid,false);
/*        Tool.ResolvedGlobalId gId= Tool.fromGlobalId(esid);
        Unit unit;
         esid自带类型了： 可能三种 Unit Company Person,直接可以判定的分叉！
        if(company)  unit=unitRepository.findUnitByCompany_Id(Long.valueOf(gId.getId()));
        else    unit=unitRepository.findUnitByPerson_Id(Long.valueOf(gId.getId()));
        return unit;*/
    }

    //地球共分七大洲continent； 前端发起本接口(continent)若参数没变的就不会重复请求的。
    @QueryMapping
    public Iterable<Country> getAllCountries(@Argument String continent) {
        List<Country>  countries=countryRepository.findAll();
        return countries;
    }
    //从已经确认的地址当中搜啊
/*
    public Connection<AddressEs> searchAddressEsFor(AddressInput where, String after, int first, String orderBy, boolean asc, DataFetchingEnvironment env) {
        DbPageConnection<AddressEs> connection=new DbPageConnection(env);
        int offset=connection.getOffset();
        int limit=connection.getLimit();
        Pageable pageable;
        if (!StringUtils.hasText(orderBy))
            pageable = PageOffsetFirst.of(offset, limit);
        else
            pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));
        Tool.ResolvedGlobalId  glId;
        List<AddressEs>  resList = new ArrayList<AddressEs>();
        BoolQueryBuilder    boolQueryBuilder=new BoolQueryBuilder();
        if (StringUtils.hasText(where.getName()))
            boolQueryBuilder.must(matchPhraseQuery("name",where.getName()).slop(50));
        //若改成 boolQueryBuilder.must(matchQuery("name",where.getName())) 可匹配出来的记录数太多了！
        if (StringUtils.hasText(where.getAdId())) {
            glId= Tool.fromGlobalId(where.getAdId());
            boolQueryBuilder.must(termQuery("ad.id", Long.valueOf(glId.getId())));
        }
        else if (StringUtils.hasText(where.getTown())) {
            glId= Tool.fromGlobalId(where.getTown());
            Town town =townRepository.findById(Long.valueOf(glId.getId())).orElse(null);
            Assert.isTrue(town !=null && null!=town.getAds(),"未找到town或Ads:"+where.getTown());
            boolQueryBuilder.must(termQuery("ad.id", town.getAds().getId() ));
        }
        else if (StringUtils.hasText(where.getCounty())) {
            glId= Tool.fromGlobalId(where.getCounty());
            boolQueryBuilder.must(termQuery("ad.countyId", Long.valueOf(glId.getId())));
        }
        else if (StringUtils.hasText(where.getCity())) {
            glId= Tool.fromGlobalId(where.getCity());
            boolQueryBuilder.must(termQuery("ad.cityId", Long.valueOf(glId.getId())));
        }
        else if (StringUtils.hasText(where.getProvince())) {
            glId= Tool.fromGlobalId(where.getProvince());
            boolQueryBuilder.must(termQuery("ad.provinceId", Long.valueOf(glId.getId())));
        }
        else if (StringUtils.hasText(where.getCountry())) {
            glId= Tool.fromGlobalId(where.getCountry());
            boolQueryBuilder.must(termQuery("ad.countryId", Long.valueOf(glId.getId())));
        }
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
                .withPageable(pageable).build();
        IndexCoordinates indexCoordinates=esTemplate.getIndexCoordinatesFor(AddressEs.class);
        SearchHits<AddressEs> searchHits = esTemplate.search(searchQuery, AddressEs.class, indexCoordinates);
        List<SearchHit<AddressEs>> hits=searchHits.getSearchHits();
        Iterable<AddressEs> list= (List<AddressEs>) SearchHitSupport.unwrapSearchHits(hits);
        //String sql=searchQuery.getQuery().toString();
        list.forEach(item -> {
            resList.add(item);
        });
        return connection.setListData(resList).get(env);
    }
*/
    /**模型JPA同义词 转换*/
    public String nodeModelMapping(Tool.ResolvedGuuid gId) {
        String model = gId.getType();
        switch (model) {
            case "EqpEs" -> model="Eqp";
            case "PipingUnitEs" -> model="PipingUnit";
            case "Boiler" -> model="Eqp";
            case "Vessel" -> model="Eqp";
            case "Elevator" -> model="Eqp";
            case "Crane" -> model="Eqp";
            case "FactoryVehicle" -> model="Eqp";
            case "Amusement" -> model="Eqp";
            case "Pipeline" -> model="Eqp";
        }
        return model;
    }
    /**模型需单独查的要；
     * 某些机制触发node接口查询的。
     * id允许为null,简化前端一边render中一边查询的不报错需求。
     * 【问题】随便一个用户比如发起查询(id="Detail:12")这样的，如何细分权限的管理! 细化Query类型接口私密性需求呢？所以需要防止泄密的模型要排除出去吧！
     * 数据库分片带来问题：需要提供分片字段取值；外部关联，json内引用ID,也需要配合分片取值;查询最好带上分片取值；GlobalId是否直接包裹分片取值type:id:routeValue？
     * 所有可走node()接口的graphQL Type模型所对应的JPA 实体类@Entity都应该实现Node interface接口类。
     * */
    @QueryMapping
    public Node node(@Argument String id) {
        if(null==id)    return null;
        Tool.ResolvedGuuid gId =null;
        try {
            gId =Tool.fromGluuId(id);
        }
        catch (Exception e) {
            assertShouldNeverHappen("不合法globalId:"+id);
            return null;
        }
        UUID lid = gId.getId();         //【前提】都是UUID作为ID的实体类仓库。
        String model = nodeModelMapping(gId);
        //用策略模式优化过多的if else语句; 超过50个的字符串匹配判定？考虑加速机制。HashMap[pt->Handler()?] Model->DbRepository 反正都是findById
        CrudRepository nodeDbRepository= nodePools.get(model);
        Assert.isTrue(null!=nodeDbRepository,"没数据仓库"+model);
        return (Node) nodeDbRepository.findById(lid).orElse(null);
    }
    /**批量读取多id; 顺序不变;
     * 多个的 Iterable<T> findAllById(Iterable<ID> ids); 幸好PipingUnit单独的，Eqp、Pipeline、EqpEs三个都是同一个Repository
     * */
    @QueryMapping
    public List<Node> modelNodes(@Argument List<String> ids) {
        if(null==ids || ids.size()==0)    return null;
        final CrudRepository[] nodeDbRepository = {null};
        //流?并行处理 多线程 有问题吗? 【前提】都是UUID作为ID的实体类仓库。
        List<UUID> uuids= ids.stream().map(id->{
            Tool.ResolvedGuuid gId =Tool.fromGluuId(id);
            String model = nodeModelMapping(gId);
            CrudRepository nodeDbRepository2= nodePools.get(model);
            Assert.isTrue(null!= nodeDbRepository2,"没数据仓库"+model);
            if(nodeDbRepository[0] ==null)
                nodeDbRepository[0] =nodeDbRepository2;
            assertTrue(nodeDbRepository[0].equals(nodeDbRepository2), () -> "批读非同一模型");
            return gId.getId();
        }).toList();
        return (List<Node>) nodeDbRepository[0].findAllById(uuids);
    }

    @QueryMapping       //测试注解 value: Float! @range(min: 1.0, max: 99.0)): Float
    public String limitedValue(@Argument Float value) {
        return "2002-11-44";
    }
}



/*执行策略ExecutionStrategy都会导致lazy失败，这里读取已跨越DB的session延迟异步读取啊，
搞成了Entity和GraphQLResolver<TYPE>两个类独立分离了，还一样Lazy懒加载错误。
像@OneToMany这类JPA注解关联关系：对方类型必须是实体的。Object对象类型不能是接口interface的。
@MappedSuperclass注解的使用 Entity类之间的继承关系    https://blog.csdn.net/zty1317313805/article/details/80524900
使用懒加载时，报异常：session失效https://blog.csdn.net/wanping321/article/details/79532918
EntityManager不会立刻关闭，导致连接池连接数占用。高并发的系统最好不要使用OpenEntityManagerInView模式；https://blog.csdn.net/q1054261752/article/details/54773428
Spring动态替换Bean 接口=BeanPostProcessor； https://www.jianshu.com/p/853a081e4a02
若toPredicate内部应用：Subquery无法一次性选择多个字段出来做表达式比较条件!!，附加新建一个CriteriaQuery却又无法连接原始query报错无法serialize;
JPA关联嵌套子查询correlate subquery；而From()是不支持子查询的。 非关联子查询指子查询可以脱离主查询独立执行；关联子查询限制是子查询不能返回多于1行的数据.
*/